import { Code } from "@/components/Code"
import { Callout } from "nextra/components"

<Callout>
  As of today, there is no built-in solution for automatic Refresh Token
  rotation. This guide will help you to achieve this in your application. Our
  goal is to add zero-config support for built-in providers eventually. [Let us
  know](/contributors#core-team) if you would like to help.
</Callout>

## What is refresh token rotation?

Refresh token rotation is the practice of updating an `access_token` on behalf of the user, without requiring interaction (ie.: re-authenticating).
`access_token`s are usually issued for a limited time. After they expire, the service verifying them will ignore the value, rendering the `access_token` useless.
Instead of asking the user to sign in again to obtain a new `access_token`, many providers also issue a `refresh_token` during initial signin, that has a longer expiry date.
Auth.js libraries can be configured to use this `refresh_token` to obtain a new `access_token` without requiring the user to sign in again.

## Implementation

<Callout type="info">
  There is an inherent limitation of the following guides that comes from the
  fact, that - for security reasons - `refresh_token`s are usually only usable
  once. Meaning that after a successful refresh, the `refresh_token` will be
  invalidated and cannot be used again. Therefore, in some cases, a
  race-condition might occur if multiple requests will try to refresh the token
  at the same time. The Auth.js team is aware of this and would like to provide
  a solution in the future. This might include some "lock" mechanism to prevent
  multiple requests from trying to refresh the token at the same time, but that
  comes with the drawback of potentially creating a bottleneck in the
  application. Another possible solution is background token refresh, to prevent
  the token from expiring during an authenticated request.
</Callout>

First, make sure that the provider you want to use supports `refresh_token`'s. Check out [The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749#section-6) spec for more details.
Depending on the [session strategy](/concepts/session-strategies), the `refresh_token` can be persisted either in an encrypted JWT inside a cookie or in a database.

### JWT strategy

<Callout>
  While using a cookie to store the `refresh_token` is simpler, it is less
  secure. To mitigate risks with the `strategy: "jwt"`, Auth.js libraries store
  the `refresh_token` in an _encrypted_ JWT, in an `HttpOnly` cookie. Still, you
  need to evaluate based on your requirements which strategy you choose.
</Callout>

Using the [jwt](/reference/core/types#jwt) and [session](/reference/core/types#session) callbacks, we can persist OAuth tokens and refresh them when they expire.

Below is a sample implementation of refreshing the `access_token` with Google. Please note that the OAuth 2.0 request to get the `refresh_token` will vary between different providers, but the rest of logic should remain similar.

<Code>
  <Code.Next>

```ts filename="./auth.ts"
import NextAuth, { type User } from "next-auth"
import Google from "next-auth/providers/google"

export const { handlers, auth } = NextAuth({
  providers: [
    Google({
      // Google requires "offline" access_type to provide a `refresh_token`
      authorization: { params: { access_type: "offline", prompt: "consent" } },
    }),
  ],
  callbacks: {
    async jwt({ token, account }) {
      if (account) {
        // First-time login, save the `access_token`, its expiry and the `refresh_token`
        return {
          ...token,
          access_token: account.access_token,
          expires_at: account.expires_at,
          refresh_token: account.refresh_token,
        }
      } else if (Date.now() < token.expires_at * 1000) {
        // Subsequent logins, but the `access_token` is still valid
        return token
      } else {
        // Subsequent logins, but the `access_token` has expired, try to refresh it
        if (!token.refresh_token) throw new TypeError("Missing refresh_token")

        try {
          // The `token_endpoint` can be found in the provider's documentation. Or if they support OIDC,
          // at their `/.well-known/openid-configuration` endpoint.
          // i.e. https://accounts.google.com/.well-known/openid-configuration
          const response = await fetch("https://oauth2.googleapis.com/token", {
            method: "POST",
            body: new URLSearchParams({
              client_id: process.env.AUTH_GOOGLE_ID!,
              client_secret: process.env.AUTH_GOOGLE_SECRET!,
              grant_type: "refresh_token",
              refresh_token: token.refresh_token!,
            }),
          })

          const tokensOrError = await response.json()

          if (!response.ok) throw tokensOrError

          const newTokens = tokensOrError as {
            access_token: string
            expires_in: number
            refresh_token?: string
          }

          return {
            ...token,
            access_token: newTokens.access_token,
            expires_at: Math.floor(Date.now() / 1000 + newTokens.expires_in),
            // Some providers only issue refresh tokens once, so preserve if we did not get a new one
            refresh_token: newTokens.refresh_token
              ? newTokens.refresh_token
              : token.refresh_token,
          }
        } catch (error) {
          console.error("Error refreshing access_token", error)
          // If we fail to refresh the token, return an error so we can handle it on the page
          token.error = "RefreshTokenError"
          return token
        }
      }
    },
    async session({ session, token }) {
      session.error = token.error
      return session
    },
  },
})

declare module "next-auth" {
  interface Session {
    error?: "RefreshTokenError"
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    access_token: string
    expires_at: number
    refresh_token?: string
    error?: "RefreshTokenError"
  }
}
```

  </Code.Next>
</Code>

### Database strategy

Using the database session strategy is similar, but instead we will save the `access_token`, `expires_at` and `refresh_token` on the `account` for the given provider.

<Code>
  <Code.Next>

```ts filename="./auth.ts"
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"

const prisma = new PrismaClient()

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    Google({
      authorization: { params: { access_type: "offline", prompt: "consent" } },
    }),
  ],
  callbacks: {
    async session({ session, user }) {
      const [googleAccount] = await prisma.account.findMany({
        where: { userId: user.id, provider: "google" },
      })
      if (googleAccount.expires_at * 1000 < Date.now()) {
        // If the access token has expired, try to refresh it
        try {
          // https://accounts.google.com/.well-known/openid-configuration
          // We need the `token_endpoint`.
          const response = await fetch("https://oauth2.googleapis.com/token", {
            method: "POST",
            body: new URLSearchParams({
              client_id: process.env.AUTH_GOOGLE_ID!,
              client_secret: process.env.AUTH_GOOGLE_SECRET!,
              grant_type: "refresh_token",
              refresh_token: googleAccount.refresh_token,
            }),
          })

          const tokensOrError = await response.json()

          if (!response.ok) throw tokensOrError

          const newTokens = tokensOrError as {
            access_token: string
            expires_in: number
            refresh_token?: string
          }

          await prisma.account.update({
            data: {
              access_token: newTokens.access_token,
              expires_at: Math.floor(Date.now() / 1000 + newTokens.expires_in),
              refresh_token:
                newTokens.refresh_token ?? googleAccount.refresh_token,
            },
            where: {
              provider_providerAccountId: {
                provider: "google",
                providerAccountId: googleAccount.providerAccountId,
              },
            },
          })
        } catch (error) {
          console.error("Error refreshing access_token", error)
          // If we fail to refresh the token, return an error so we can handle it on the page
          session.error = "RefreshTokenError"
        }
      }
      return session
    },
  },
})

declare module "next-auth" {
  interface Session {
    error?: "RefreshTokenError"
  }
}
```

  </Code.Next>
</Code>

### Error handling

If the token refresh was unsuccesful, we can force a re-authentication.

<Code>

<Code.Next>

```tsx filename="app/dashboard/page.tsx"
import { auth, signIn } from "@/auth"

export default async function Page() {
  const session = await auth()
  if (session?.error === "RefreshTokenError") {
    await signIn("google") // Force sign in to obtain a new set of access and refresh tokens
  }
}
```

</Code.Next>

<Code.NextClient>

```tsx filename="app/dashboard/page.tsx"
"use client"

import { useEffect } from "react"
import { signIn, useSession } from "next-auth/react"

export default function Page() {
  const { data: session } = useSession() // For this to work, the Page should be wrapped inside the SessionProvider component in Layout
  useEffect(() => {
    if (session?.error !== "RefreshTokenError") return
    signIn("google") // Force sign in to obtain a new set of access and refresh tokens
  }, [session?.error])
}
```

</Code.NextClient>

</Code>
